#!/usr/bin/env bash
# Copyright (c) 2021 The Toltec Contributors
# SPDX-License-Identifier: MIT

#
# bootstrap
#
# Install and configure Opkg, Entware and Toltec
#
# Arguments: Additional packages to install after setting up Toltec
# Example: ./bootstrap harmony draft
#
# Based on Evan Widloski’s remarkable_entware (2019-03-21)
# Based on the Entware installer from
# <http://bin.entware.net/armv7sf-k3.2/installer/generic.sh>
#

if [ -z "$BASH" ] || [[ "$(ps | awk '$1=='$$' { n=split($5,a,"/"); print a[n] }')" != "bash" ]]; then
    echo "bootstrap must be run with bash"
    exit 1
fi

set -eEuo pipefail

# Path to the temporary local wget and Opkg binaries
wget_path="${wget_path:-/home/root/.local/bin/wget}"
opkg_path="${opkg_path:-/home/root/.local/bin/opkg}"

# Path to the script used for managing the Toltec installation
toltecctl_path="${toltecctl_path:-/home/root/.local/bin/toltecctl}"

# Toltec branch to use for this install - you almost always want the default
toltec_branch="${toltec_branch:-stable}"

# Base URLs for bootstrapping from the Entware and Toltec repositories
entware_remote="${entware_remote:-https://bin.entware.net/armv7sf-k3.2/installer}"
toltec_remote="${toltec_remote:-https://toltec-dev.org/$toltec_branch/rmall}"

# Remove all temporary files
cleanup() {
    rm -f "$wget_path" "$opkg_path"
}

# Remove unfinished installation after unexpected error
error-cleanup() {
    read -ra trace < <(caller 0)
    log ERROR "Unexpected error on line ${trace[2]}:${trace[0]} in function ${trace[1]}"
    log ERROR "This script failed to install. If you can't solve the above" \
        "issue yourself, please report it at:" \
        "" \
        "    <https://github.com/toltec-dev/toltec/issues>" \
        "" \
        "(Please also include these error logs to help solving the" \
        "problem faster. Thank you!)"

    # Get out of /opt so it can be unmounted and removed
    cd /home/root

    clean-path
    remove-bind-mount "$toltec_dest"
    rm -rf "$toltec_dest" "$toltec_src" "$toltecctl_path"
}

# Check that a directory exists and is not empty
exists-non-empty() {
    [[ -d $1 ]] && files="$(ls -A -- "$1")" && [[ -n $files ]]
}

already-installed-message() {
    log "Toltec is already installed or partially installed"
    log "To re-enable Toltec after a system upgrade, run 'toltecctl reenable'"
    log "To reinstall Toltec, run 'toltecctl uninstall' first"
}

# Check whether a Toltec install already exists or if conflicting files
# remain from previous installs
check-installed() {
    local unit_path
    local unit
    # This should mimic get-bind-mount-path as close as possible to ensure it's always
    # the path that an install or reenable would generate.
    unit_path="/lib/systemd/system/$(systemd-escape --path /opt).mount"
    unit="$(basename "$unit_path")"
    if [[ -f $unit_path ]] && systemctl --quiet is-active "$unit" 2> /dev/null; then
        log ERROR "opt.mount is currently active"
        already-installed-message
        exit 1
    fi
    if [[ "$(grep ' /opt ' /proc/mounts)" != "" ]]; then
        log ERROR "/opt is currently mounted"
        already-installed-message
        exit 1
    fi
    if exists-non-empty /opt; then
        log ERROR "/opt exists and is not empty"
        already-installed-message
        exit 1
    fi
    if exists-non-empty /home/root/.entware; then
        log ERROR "/home/root/.entware exists and is not empty"
        already-installed-message
        exit 1
    fi
}

# Install a local wget binary which supports TLS (the original one
# installed on the reMarkable does not) in the PATH
wget-bootstrap() {
    local wget_remote=http://toltec-dev.org/thirdparty/bin/wget-v1.21.1-1
    local wget_checksum=c258140f059d16d24503c62c1fdf747ca843fe4ba8fcd464a6e6bda8c3bbb6b5

    if [[ ! -x $wget_path ]]; then
        if [[ -e $wget_path ]]; then
            log ERROR "'$wget_path' exists, but is not executable"
            log ERROR "You may need to delete it and try again."
            exit 1
        fi

        log INFO "Fetching secure wget"

        # Download and compare to hash
        mkdir -p "$(dirname "$wget_path")"

        if ! wget -q "$wget_remote" --output-document "$wget_path"; then
            log ERROR "Could not fetch wget, make sure you have a stable Wi-Fi connection"
            exit 1
        fi

        if ! sha256sum -c <(echo "$wget_checksum  $wget_path") > /dev/null 2>&1; then
            log ERROR "Invalid checksum for the local wget binary"
            exit 1
        fi

        chmod 755 "$wget_path"
    fi

    # Ensure the local binary is used
    if [[ $(command -v wget) != "$wget_path" ]]; then
        export PATH
        PATH="$(dirname "$wget_path"):$PATH"
    fi
}

# Install a temporary Opkg binary in PATH to be able to install
# bootstrapping packages
opkg-bootstrap() {
    log "Bootstrapping Opkg"
    mkdir -p /opt/tmp

    if [[ ! -x $opkg_path ]]; then
        if [[ -e $opkg_path ]]; then
            log ERROR "'$opkg_path' exists, but is not executable"
            log ERROR "You may need to delete it and try again."
            exit 1
        fi

        if ! wget --no-verbose "$entware_remote/opkg" --output-document "$opkg_path"; then
            log ERROR "Could not fetch opkg, make sure you have a stable Wi-Fi connection"
            exit 1
        fi

        chmod 755 "$opkg_path"
    fi

    # Ensure the local binary is used
    if [[ $(command -v opkg) != "$opkg_path" ]]; then
        export PATH
        PATH="$(dirname "$opkg_path"):$PATH)"
    fi
}

# Install Toltec and Entware to /opt
main() {
    # Make sure there’s no existing Toltec install
    # (otherwise it could get wiped out by the error-cleanup hook below)
    check-installed

    # Fetch temporary wget and opkg binaries used for bootstrapping
    wget-bootstrap
    opkg-bootstrap

    # Remove those binaries in any case when the script exits
    trap cleanup EXIT

    # Download and install the latest toltec-bootstrap package (without
    # its dependencies) to use toltecctl definitions
    local pkg_basename
    pkg_basename="$(
        wget --quiet "$toltec_remote/Packages" -O - \
            | awk '/^Package: toltec-bootstrap$/,/^$/ { if ($1 == "Filename:") print $2 }'
    )"
    local pkg_filename="/tmp/$pkg_basename"
    wget --no-verbose "$toltec_remote/$pkg_basename" -O "$pkg_filename"

    tar -zxvOf "$pkg_filename" ./data.tar.gz | tar -zxf - -C /
    rm "$pkg_filename"

    # shellcheck source=../../package/toltec-bootstrap/toltecctl
    source /home/root/.local/bin/toltecctl

    # Clean up the partial install if an uncaught error happens
    trap error-cleanup ERR

    if ! check-version "$toltec_branch"; then
        if [ $# -eq 0 ] || [[ "$1" != "--force" ]]; then
            exit 1
        fi
    fi

    log "Installing Toltec and Entware"

    # Create bind mount from user directory to /opt
    rm -rf /opt
    add-bind-mount "$toltec_src" "$toltec_dest"
    mkdir -p /opt/{bin,etc,lib/opkg,tmp,var/lock}

    # Generate Opkg configuration
    create-entware-conf
    switch-branch "$toltec_branch"
    generate-opkg-conf

    # Install base Entware packages
    opkg update
    opkg install entware-opt

    # Fix for multiuser environment
    chmod 777 /opt/tmp

    # Create basic administrative files
    if [[ -f /etc/passwd ]]; then
        ln -sf /etc/passwd /opt/etc/passwd
    else
        cp /opt/etc/passwd.1 /opt/etc/passwd
    fi

    if [[ -f /etc/group ]]; then
        ln -sf /etc/group /opt/etc/group
    else
        cp /opt/etc/group.1 /opt/etc/group
    fi

    if [[ -f /etc/shells ]]; then
        ln -sf /etc/shells /opt/etc/shells
    else
        cp /opt/etc/shells.1 /opt/etc/shells
    fi

    if [[ -f /etc/shadow ]]; then
        ln -sf /etc/shadow /opt/etc/shadow
    fi

    if [[ -f /etc/gshadow ]]; then
        ln -sf /etc/gshadow /opt/etc/gshadow
    fi

    if [[ -f /etc/localtime ]]; then
        ln -sf /etc/localtime /opt/etc/localtime
    fi

    opkg install toltec-base

    if [[ $# -gt 0 ]] && ! /opt/bin/opkg install "$@"; then
        log ERROR "The install was still successful, but the requested packages failed to install"
    fi

    log "After each system upgrade, run 'toltecctl reenable' to re-enable Toltec"
}

# Print a log message
#
# Arguments:
#
# [$1] - Log level: INFO, WARN or ERROR (default: INFO)
# $2... - Messages to print, each argument goes to a separate line
log() {
    # Output stream where the messages will be sent
    local log_type="INFO"
    local fd=1
    local colored_prefix

    if [[ $# -ge 2 ]]; then
        case "$1" in
            INFO | WARN | ERROR)
                log_type="$1"
                shift
                ;;
        esac
    fi

    case "$log_type" in
        INFO) colored_prefix='\e[32mINFO:\e[0m  ' ;;
        WARN)
            colored_prefix='\e[33mWARN:\e[0m  '
            fd=2
            ;;
        ERROR)
            colored_prefix='\e[31mERROR:\e[0m '
            fd=2
            ;;
    esac

    echo -e "${colored_prefix}$1" >&$fd

    # Extra lines to print indented
    shift
    local line

    for line in "$@"; do
        echo -e "       $line" >&$fd
    done
}

main "$@"
